//******************************************************************
//******************************************************************
//**********          ANts Peer To Peer Sources        *************
//
// ANts P2P realizes a third generation P2P net. It protects your
// privacy while you are connected and makes you not trackable, hiding
// your identity (ip) and crypting everything you are sending/receiving
// from others.

// Copyright (C) 2004  Roberto Rossi

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

package ants.p2p.utils.net;

import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.Random;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.Argument;
import org.cybergarage.upnp.ControlPoint;
import org.cybergarage.upnp.Device;
import org.cybergarage.upnp.UPnPStatus;
import org.cybergarage.upnp.DeviceList;
import org.cybergarage.upnp.Service;
import org.cybergarage.upnp.device.DeviceChangeListener;
import org.cybergarage.upnp.control.*;

import ants.p2p.gui.FrameAnt;
import ants.p2p.messages.security.MessageSigner;

import org.apache.log4j.*;
import org.cybergarage.upnp.StateVariable;
import java.io.File;
import org.cybergarage.upnp.device.InvalidDescriptionException;
import org.cybergarage.upnp.ServiceList;
import org.cybergarage.upnp.UPnP;
import ants.p2p.filesharing.WarriorAnt;
import org.cybergarage.xml.parser.kXML2Parser;

/**
 * 
 * According to the UPnP Standards, Internet Gateway Devices must have a
 * specific hierarchy. The parts of that hierarchy that we care about are:
 * 
 * Device: urn:schemas-upnp-org:device:InternetGatewayDevice:1 SubDevice:
 * urn:schemas-upnp-org:device:WANDevice:1 SubDevice:
 * urn:schemas-upnp-org:device:WANConnectionDevice:1 Service:
 * urn:schemas-upnp-org:service:WANIPConnection:1
 * 
 * Every port mapping is a tuple of: - External address ("" is wildcard) -
 * External port - Internal address - Internal port - Protocol (TCP|UDP) -
 * Description
 * 
 * Port mappings can be removed, but that is a blocking network operation which
 * will slow down the shutdown process of Limewire. It is safe to let port
 * mappings persist between limewire sessions. In the meantime however, the NAT
 * may assign a different ip address to the local node. That's why we need to
 * find any previous mappings the node has created and update them with our new
 * address. In order to uniquely distinguish which mappings were made by us, we
 * put part of our client GUID in the description field.
 * 
 * For the TCP mapping, we use the following description: "ANts/TCP:<cliengGUID>"
 * 
 * NOTES:
 * 
 * Not all NATs support mappings with different external port and internal
 * ports. Therefore if we were unable to map our desired port but were able to
 * map another one, we should pass this information on to Acceptor.
 * 
 * Some buggy NATs do not distinguish mappings by the Protocol field. Therefore
 * we first map the UDP port, and then the TCP port since it is more important
 * should the first mapping get overwritten.
 * 
 * The cyberlink library uses an internal thread that tries to discover any UPnP
 * devices. After we discover a router or give up on trying to, we should call
 * stop().
 * 
 */
public class UPnPManager extends ControlPoint implements DeviceChangeListener,
		ActionListener, QueryListener {

	private static Logger _logger = Logger.getLogger(UPnPManager.class
			.getName());

	private final static String DESCRIPTION_FILE_NAME = WarriorAnt.workingPath
			+ "upnpdescriptor/description.xml";
	private final static String PRESENTATION_URI = "http://www.kerjodando.com";

	public final static String ANTS_DEVICE_TYPE = "urn:schemas-upnp-org:device:kerjodando:1";
	public final static String ANTS_LAN_ADDRESS_SERVICE_TYPE = "urn:schemas-upnp-org:service:address:1";
	public final static String ANTS_LAN_ADDRESS_ACTION_TYPE = "GetLANAddress";

	private StateVariable serverInfo;

	private Device antsDev = null;

	/** some schemas */
	private static final String ROUTER_DEVICE = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
	private static final String WAN_DEVICE = "urn:schemas-upnp-org:device:WANDevice:1";
	private static final String WANCON_DEVICE = "urn:schemas-upnp-org:device:WANConnectionDevice:1";
	private static final String SERVICE_TYPE = "urn:schemas-upnp-org:service:WANIPConnection:1";

	static {
		UPnP.setXMLParser(new org.cybergarage.xml.parser.kXML2Parser());
	}
	/** prefixes and a suffix for the descriptions of our TCP and UDP mappings */
	private static final String TCP_PREFIX = "KerjoTCP";
	// private static final String UDP_PREFIX = "ANtsUDP";
	private String _guidSuffix;

	private static UPnPManager INSTANCE = null;

	public static synchronized UPnPManager instance() {
		if (INSTANCE == null) {
			UPnPDescriptor.createDirectoryStructure();
			return INSTANCE = new UPnPManager();
		} else {
			return INSTANCE;
		}
	}

	/**
	 * the router we have and the sub-device necessary for port mapping LOCKING:
	 * this
	 */
	private Device _router;

	/**
	 * The port-mapping service we'll use. LOCKING: this
	 */
	private Service _service;

	/** The tcp and udp mappings created this session */
	private Mapping _tcp;// , _udp;

	private UPnPManager() {
		super();
		_logger.info("Starting UPnP Service Manager.");
		try {
			addDeviceChangeListener(this);
			start();
		} catch (Exception bad) {
			bad.printStackTrace();
		}
	}

	public Device getDevice() {
		return this.antsDev;
	}

	public void setCurrentLanAddress(ants.p2p.query.ServerInfo si) {
		try {
			if (si == null || this.serverInfo == null)
				return;
			this.serverInfo.setValue(si.toString());
		} catch (Exception e) {
			_logger.error("Error in setting UPnP LAN address", e);
		}
	}

	public ants.p2p.query.ServerInfo getCurrentLanAddress() {
		try {
			if (this.serverInfo == null)
				return null;
			String[] addressPort = this.serverInfo.getValue().split(":");
			ants.p2p.query.ServerInfo si = new ants.p2p.query.ServerInfo("",
					addressPort[0], new Integer(addressPort[1]), "");
			return si;
		} catch (Exception e) {
			_logger.error("Error in retrieving UPnP LAN address", e);
			return null;
		}
	}

	public void startDevice() {
		stopDevice();
		try {
			antsDev = new Device(DESCRIPTION_FILE_NAME);

			Action getTimeAction = antsDev.getAction("GetLANAddress");
			getTimeAction.setActionListener(this);

			ServiceList serviceList = antsDev.getServiceList();
			Service service = serviceList.getService(0);
			service.setQueryListener(this);

			serverInfo = antsDev.getStateVariable("LANAddress");

			antsDev.setLeaseTime(60);
			antsDev.start();
		} catch (InvalidDescriptionException e) {
			_logger.error(
					"Invalid description file for Kerjodando UPnP Service", e);
		}
	}

	public void stopDevice() {
		if (antsDev != null) {
			antsDev.byebye();
			antsDev.stop();
			this.antsDev = null;
			this.serverInfo = null;
		}
	}

	public boolean queryControlReceived(StateVariable stateVar) {
		// MUST SET THE LAN ADDRESS HERE (SERVER INFO)!!!!
		stateVar.setValue(serverInfo.getValue());
		return true;
	}

	public boolean actionControlReceived(Action action) {
		String actionName = action.getName();
		if (actionName.equals("GetLANAddress") == true) {
			Argument timeArg = action.getArgument("LANAddress");
			timeArg.setValue(serverInfo.getValue());
			return true;
		}
		return false;
	}

	public void update(String newValue) {
		serverInfo.setValue(newValue);
	}

	/**
	 * @return whether we are behind an UPnP-enabled NAT/router
	 */
	public synchronized boolean isNATPresent() {
		return _router != null && _service != null;
	}

	/**
	 * @return whether we have created mappings this session
	 */
	public/* synchronized */boolean mappingsExist() {
		return _tcp != null /* && _udp != null */;
	}

	/**
	 * @return the external address the NAT thinks we have. Blocking. null if we
	 *         can't find it.
	 */
	public InetAddress getNATAddress() throws UnknownHostException {
		Action getIP;

		synchronized (this) {
			if (!isNATPresent())
				return null;
			getIP = _service.getAction("GetExternalIPAddress");
		}

		if (getIP == null) {
			_logger.info("Couldn't find GetExternalIPAddress action!");
			return null;
		}

		if (!getIP.postControlAction()) {
			_logger.info("couldn't get our external address");
			return null;
		}

		Argument ret = getIP.getOutputArgumentList().getArgument(
				"NewExternalIPAddress");
		return InetAddress.getByName(ret.getValue());
	}

	/**
	 * this method will be called when we discover a UPnP device.
	 */
	public synchronized void deviceAdded(Device dev) {
		_logger.info("Device added: " + dev.getFriendlyName());

		// we've found what we need
		if (_service != null && _router != null) {
			_logger.info("we already have a router");
			return;
		}

		// did we find a router?
		if (dev.getDeviceType().equals(ROUTER_DEVICE) && dev.isRootDevice())
			_router = dev;

		if (_router == null) {
			_logger.info("didn't get router device");
			return;
		}

		discoverService();

		// did we find the service we need?
		if (_service == null) {
			_logger.info("couldn't find service");
			_router = null;
		} else {
			_logger.info("Found service, router: " + _router.getFriendlyName()
					+ ", service: " + _service);
			// stop();
		}
	}

	/**
	 * Traverses the structure of the router device looking for the port mapping
	 * service.
	 */
	private void discoverService() {

		for (Iterator iter = _router.getDeviceList().iterator(); iter.hasNext();) {
			Device current = (Device) iter.next();
			if (!current.getDeviceType().equals(WAN_DEVICE))
				continue;

			DeviceList l = current.getDeviceList();
			_logger.info("found " + current.getDeviceType() + ", size: "
					+ l.size() + ", on: " + current.getFriendlyName());

			for (int i = 0; i < current.getDeviceList().size(); i++) {
				Device current2 = l.getDevice(i);

				if (!current2.getDeviceType().equals(WANCON_DEVICE))
					continue;

				_logger.info("found " + current2.getDeviceType() + ", on: "
						+ current2.getFriendlyName());

				_service = current2.getService(SERVICE_TYPE);
				return;
			}
		}
	}

	/**
	 * adds a mapping on the router to the specified port
	 * 
	 * @return the external port that was actually mapped. 0 if failed
	 */
	public int mapPort(int port) {
		_logger.info("Attempting to map port: " + port);

		Random gen = null;

		// modified by casper
		// todo , the ip address should be the same as which kerjo is listening
		// on
		// String localAddress = FrameAnt.getInstance(null).getGuiAnt()
		// .getConnectionAntPanel().getLocalStringAddress();
		ants.p2p.query.ServerInfo si = getCurrentLanAddress();
		if (si == null) {
			return 0;

		}
		String localAddress = si.getAddress().getHostAddress();
		int localPort = port;

		// try adding new mappings with the same port
		/*
		 * Mapping udp = new Mapping("", port, localAddress, localPort, "UDP",
		 * UDP_PREFIX + getGUIDSuffix()); // add udp first in case it gets
		 * overwritten. // if we can't add, update or find an appropriate port //
		 * give up after 20 tries int tries = 20; while (!addMapping(udp)) { if
		 * (tries<0) break; tries--; // try a random port if (gen == null) gen =
		 * new Random(); port = gen.nextInt(50000)+2000; udp = new Mapping("",
		 * port, localAddress, localPort, "UDP", UDP_PREFIX + getGUIDSuffix()); }
		 * 
		 * if (tries < 0) { _logger.info("couldn't map a port :("); return 0; }
		 */

		// at this stage, the variable port will point to the port the UDP
		// mapping
		// got mapped to. Since we have to have the same port for UDP and tcp,
		// we can't afford to change the port here. So if mapping to this port
		// on tcp
		// fails, we give up and clean up the udp mapping.
		Mapping tcp = new Mapping("", port, localAddress, localPort, "TCP",
				TCP_PREFIX + getGUIDSuffix());
		//my router have a chance to be success
		// other operation behaves the same.
		int tries=100;
		while(tries>0 && !addMapping(tcp))tries--;
		if (tries==0) {
			_logger
					.info(" couldn't map tcp. cleaning up...");
			port = 0;
			tcp = null;
			// udp = null;
		}

		// save a ref to the mappings
		synchronized (this) {
			_tcp = tcp;
			// _udp = udp;
		}

		// we're good - start a thread to clean up any potentially stale
		// mappings
		Thread staleCleaner = new Thread(new StaleCleaner());
		staleCleaner.setDaemon(true);
		staleCleaner.setName("Stale Mapping Cleaner");
		staleCleaner.start();

		return port;
	}

	/**
	 * @param m
	 *            Port mapping to send to the NAT
	 * @return the error code
	 */
	private boolean addMapping(Mapping m) {

		_logger.info("adding " + m);

		Action add;
		synchronized (this) {
			add = _service.getAction("AddPortMapping");
		}

		if (add == null) {
			_logger.info("Couldn't find AddPortMapping action!");
			return false;
		}

		//org.cybergarage.util.Debug.on();
		add.setArgumentValue("NewRemoteHost", m._externalAddress);
		add.setArgumentValue("NewExternalPort", m._externalPort);
		add.setArgumentValue("NewInternalClient", m._internalAddress);
		add.setArgumentValue("NewInternalPort", m._internalPort);
		add.setArgumentValue("NewProtocol", m._protocol);
		add.setArgumentValue("NewPortMappingDescription", m._description);
		add.setArgumentValue("NewEnabled", "1");
		add.setArgumentValue("NewLeaseDuration", 0);

		boolean success = add.postControlAction();
		/*if (!success) {
			UPnPStatus err = add.getControlStatus();
			_logger.error(err.getDescription());
		}*/
		_logger.info("Post succeeded: " + success);
		//org.cybergarage.util.Debug.off();
		return success;
	}

	/**
	 * @param m
	 *            the mapping to remove from the NAT
	 * @return whether it worked or not
	 */
	private boolean removeMapping(Mapping m) {

		_logger.info("removing " + m);

		Action remove;
		synchronized (this) {
			remove = _service.getAction("DeletePortMapping");
		}

		if (remove == null) {
			_logger.info("Couldn't find DeletePortMapping action!");
			return false;
		}

		remove.setArgumentValue("NewRemoteHost", m._externalAddress);
		remove.setArgumentValue("NewExternalPort", m._externalPort);
		remove.setArgumentValue("NewProtocol", m._protocol);

		boolean success = remove.postControlAction();
		_logger.info("Remove succeeded: " + success);
		return success;
	}

	/**
	 * schedules a shutdown hook which will clear the mappings created this
	 * session.
	 */
	public void clearMappingsOnShutdown() {
		final Mapping tcp/* , udp */;
		synchronized (this) {
			tcp = _tcp;
			// udp = _udp;
		}

		final Thread cleaner = new Thread() {
			public void run() {
				_logger.info("start cleaning");
				removeMapping(tcp);
				// removeMapping(udp);
				_logger.info("done cleaning");
			}
		};

		Thread waiter = new Thread() {
			public void run() {
				try {
					_logger.info("waiting for UPnP cleaners to finish");
					cleaner.join(30000); // wait at most 30 seconds.
				} catch (InterruptedException ignored) {
				}
				_logger.info("UPnP cleaners done");
			}
		};
		waiter.setName("shutdown mapping waiter");

		try {
			Runtime.getRuntime().addShutdownHook(waiter);
		} catch (IllegalStateException ignored) {
		}

		cleaner.setName("shutdown mapping cleaner");
		cleaner.setDaemon(true);
		cleaner.start();
		Thread.yield(); // let it start.
	}

	public void finalize() {
		stop();
	}

	private synchronized String getGUIDSuffix() {
		if (_guidSuffix == null) {
			try {
				_guidSuffix = MessageSigner.getInstance().getPublicKey()
						.substring(0, 10);
			} catch (Exception e) {
			}
		}

		// the bellow codes are totally wrong by casper 08-02-22
		// if (_guidSuffix == null)
		// FrameAnt.getInstance(null).getGuiAnt().getConnectionAntPanel().getWarriorAnt().getIdent().substring(0,10);
		return _guidSuffix;
	}

	/**
	 * stub
	 */
	public void deviceRemoved(Device dev) {
	}

	private final class Mapping {
		public final String _externalAddress;
		public final int _externalPort;
		public final String _internalAddress;
		public final int _internalPort;
		public final String _protocol, _description;

		// network constructor
		public Mapping(String externalAddress, String externalPort,
				String internalAddress, String internalPort, String protocol,
				String description) throws NumberFormatException {
			_externalAddress = externalAddress;
			_externalPort = Integer.parseInt(externalPort);
			_internalAddress = internalAddress;
			_internalPort = Integer.parseInt(internalPort);
			_protocol = protocol;
			_description = description;
		}

		// internal constructor
		public Mapping(String externalAddress, int externalPort,
				String internalAddress, int internalPort, String protocol,
				String description) {
			_externalAddress = externalAddress;
			_externalPort = externalPort;
			_internalAddress = internalAddress;
			_internalPort = internalPort;
			_protocol = protocol;
			_description = description;
		}

		public String toString() {
			return _externalAddress + ":" + _externalPort + "->"
					+ _internalAddress + ":" + _internalPort + "@" + _protocol
					+ " desc: " + _description;
		}

	}

	/**
	 * This thread reads all the existing mappings on the NAT and if it finds a
	 * mapping which appears to be created by us but points to a different
	 * address (i.e. is stale) it removes the mapping.
	 * 
	 * It can take several minutes to finish, depending on how many mappings
	 * there are.
	 */
	private class StaleCleaner implements Runnable {

		// TODO: remove
		private String list(java.util.List l) {
			String s = "";
			for (Iterator i = l.iterator(); i.hasNext();) {
				Argument next = (Argument) i.next();
				s += next.getName() + "->" + next.getValue() + ", ";
			}
			return s;
		}

		public void run() {

			_logger.info("Looking for stale mappings...");

			Set mappings = new HashSet();
			Action getGeneric;
			synchronized (UPnPManager.this) {
				getGeneric = _service.getAction("GetGenericPortMappingEntry");
			}

			if (getGeneric == null) {
				_logger
						.info("Couldn't find GetGenericPortMappingEntry action!");
				return;
			}

			// get all the mappings
			try {
				for (int i = 0;; i++) {
					getGeneric.setArgumentValue("NewPortMappingIndex", i);
					_logger.info("Stale Iteration: " + i + ", generic.input: "
							+ list(getGeneric.getInputArgumentList())
							+ ", generic.output: "
							+ list(getGeneric.getOutputArgumentList()));

					if (!getGeneric.postControlAction())
						break;

					mappings.add(new Mapping(getGeneric
							.getArgumentValue("NewRemoteHost"), getGeneric
							.getArgumentValue("NewExternalPort"), getGeneric
							.getArgumentValue("NewInternalClient"), getGeneric
							.getArgumentValue("NewInternalPort"), getGeneric
							.getArgumentValue("NewProtocol"), getGeneric
							.getArgumentValue("NewPortMappingDescription")));
					// TODO: erase output arguments.

				}
			} catch (NumberFormatException bad) {
				_logger.error("NFE reading mappings!", bad);
				// router broke.. can't do anything.
				return;
			}

			_logger.info("Stale cleaner found " + mappings.size()
					+ " total mappings");

			// iterate and clean up
			for (Iterator iter = mappings.iterator(); iter.hasNext();) {
				Mapping current = (Mapping) iter.next();
				_logger.info("Analyzing: " + current);

				if (current._description == null)
					continue;

				// does it have our description?
				if (current._description.equals(TCP_PREFIX + getGUIDSuffix())/*
																				 * ||
																				 * current._description.equals(UDP_PREFIX+getGUIDSuffix())
																				 */) {

					// is it not the same as the mappings we created this
					// session?
					synchronized (this) {

						if (_tcp != null
								&& /* _udp != null && */
								current._externalPort == _tcp._externalPort
								&& current._internalAddress
										.equals(_tcp._internalAddress)
								&& current._internalPort == _tcp._internalPort)
							continue;
					}

					// remove it.
					_logger.info("mapping " + current + " appears to be stale");
					removeMapping(current);
				}
			}
		}
	}
}
